import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:hex/hex.dart';
import 'package:eciesdart/src/bigint.dart';
import 'package:pointycastle/export.dart';
import 'package:bip39/bip39.dart' as bip39;

typedef KeyPairBytes = ({Uint8List publicKey, Uint8List privateKey});

const int _uncompressedPublicKeySize = 65;
// 16 bits to match the js implementation https://github.com/ecies/js/blob/f7f0923362beea9e0c4e05c2bcf5bceb1980f9e5/src/config.ts#L19
const int aesIvLength = 16;
const int _aesTagLength = 16;
// 32 bytes for 256 bit encryption
const int _secretKeyLength = 32;
final _sGen = Random.secure();
final _seed = Uint8List.fromList(List.generate(32, (n) => _sGen.nextInt(255)));
final _secureRandom = SecureRandom('Fortuna')..seed(KeyParameter(_seed));

class Secp256R1Util {

  /// Encrypt [message].
  ///
  /// Encrypt [message] given a [publicKey] ASN.1 BigInt and return a buffer with the
  /// format
  /// ```
  /// [
  /// ...uncompressedPublicKey, // 65 bytes
  /// ...iv,                    // 12 or 16 bytes,
  /// ...cipherText,            // variable length
  /// ...tag                    // 16 bytes
  /// ]
  /// ```
  /// Optionally, you can specify the [ivLength] as either 12 or 16 bytes.
  static Uint8List encrypt(Uint8List publicKey, Uint8List message,
      {int ivLength = aesIvLength}) {
    validateIVLength(ivLength);
    final ecSpec = ECKeyGeneratorParameters(ECCurve_secp256r1());
    final ephemeralKeyPair = _generateEphemeralKey(ecSpec);
    ECPrivateKey ephemeralPrivKey = ephemeralKeyPair.privateKey as ECPrivateKey;
    ECPublicKey ephemeralPubKey = ephemeralKeyPair.publicKey as ECPublicKey;
    // Generate receiver PK
    ECPublicKey ecPublicKey = _getEcPublicKey(ecSpec.domainParameters, publicKey);

    // Derive shared secret
    final uncompressed = ephemeralPubKey.Q!.getEncoded(false);
    final multiply = ecPublicKey.Q! * ephemeralPrivKey.d;
    final aesKey = _hkdf(uncompressed, multiply!.getEncoded(false));
    return _aesEncrypt(message, ephemeralPubKey, aesKey, ivLength: ivLength);
  }

  /// Decrypt [message].
  ///
  /// Decrypt [message] given a [privateKey] ASN.1 BigInt. The input buffer must have the
  /// following format
  /// ```
  /// [
  /// ...uncompressedPublicKey, // 65 bytes
  /// ...iv,                    // 12 or 16 bytes,
  /// ...cipherText,            // variable length
  /// ...tag                    // 16 bytes
  /// ]
  /// ```
  /// The [ivLength] must match the length used for encryption and defaults to 16 bytes.
  static Uint8List decrypt(Uint8List privateKey, Uint8List message,
      {int ivLength = aesIvLength}) {
    validateIVLength(ivLength);
    final keyParams = ECCurve_secp256r1();
    final ecSpec = ECKeyGeneratorParameters(keyParams);

    // Generate receiver private key
    final receiverSK = bytesToBigInt(privateKey);
    ECPrivateKey ecPrivateKey = ECPrivateKey(receiverSK, ecSpec.domainParameters);

    final senderPK = message.sublist(0, _uncompressedPublicKeySize);
    final Q = ecSpec.domainParameters.curve.decodePoint(senderPK);
    final senderPK2 = ECPublicKey(Q, ecSpec.domainParameters);

    // Decapsulate
    final uncompressed = senderPK2.Q!.getEncoded(false);
    final multiply = senderPK2.Q! * ecPrivateKey.d;
    final aesKey = _hkdf(uncompressed, multiply!.getEncoded(false));

    return _aesDecrypt(message, aesKey, ivLength: ivLength);
  }

  /// Generate an EC key pair.
  ///
  /// Generate an EC key pair using the secp256r1 curve.
  static AsymmetricKeyPair<ECPublicKey, ECPrivateKey> generateEcKeyPair() {
    final keyParams = ECCurve_secp256r1();
    final ecSpec = ECKeyGeneratorParameters(keyParams);
    final keyGenerator = ECKeyGenerator()
      ..init(ParametersWithRandom(ecSpec, _secureRandom));
    final pair = keyGenerator.generateKeyPair();
    final publicKey = pair.publicKey as ECPublicKey;
    final privateKey = pair.privateKey as ECPrivateKey;
    return AsymmetricKeyPair(publicKey, privateKey);
  }

  /// Generate an EC key pair.
  ///
  /// Generate an EC key pair using the secp256r1 curve and return the ASN.1
  /// representation of the [BigInt]s
  static KeyPairBytes generateEcKeyPairBytes() {
    final pair = generateEcKeyPair();
    return (
    publicKey: pair.publicKey.Q!.getEncoded(false),
    privateKey: bigIntToBytes(pair.privateKey.d!)
    );
  }

  /// Encrypt a [message] using AES-256-GCM.
  ///
  /// Encrypt a [message] and return a buffer in the following format
  /// ```
  /// [
  /// ...uncompressedPublicKey, // 65 bytes
  /// ...iv,                    // 12 or 16 bytes,
  /// ...cipherText,            // variable length
  /// ...tag                    // 16 bytes
  /// ]
  /// ```
  static Uint8List _aesEncrypt(Uint8List message, ECPublicKey ephemeralPubKey, Uint8List aesKey, {required int ivLength}) {
    final iv = _secureRandom.nextBytes(ivLength);
    final cipher = GCMBlockCipher(AESEngine());
    cipher.init(
      true,
      AEADParameters(
        KeyParameter(aesKey),
        _aesTagLength * 8,
        iv,
        Uint8List(0),
      ),
    );

    final ephemeralPkUncompressed = ephemeralPubKey.Q!.getEncoded(false);
    final cipherTextAndTag = cipher.process(message);
    final cipherTextLength = cipherTextAndTag.length - _aesTagLength;
    final tag = cipherTextAndTag.sublist(cipherTextLength);
    final cipherText = cipherTextAndTag.sublist(0, cipherTextLength);

    final result = Uint8List.fromList(
        [...ephemeralPkUncompressed, ...iv, ...tag, ...cipherText]);
    return result;
  }

  /// Decrypt  [inputBytes] using AES-256-GCM.
  ///
  /// Decrypt [inputBytes] with the following format
  /// ```
  /// [
  /// ...uncompressedPublicKey, // 65 bytes
  /// ...iv,                    // 12 or 16 bytes,
  /// ...cipherText,            // variable length
  /// ...tag                    // 16 bytes
  /// ]
  /// ```
  static Uint8List _aesDecrypt(Uint8List inputBytes, Uint8List aesKey,
      {required int ivLength}) {
    final ivTagAndCipherText = inputBytes.sublist(_uncompressedPublicKeySize);
    final iv = ivTagAndCipherText.sublist(0, ivLength);
    final tagAndCipherText = ivTagAndCipherText.sublist(ivLength);
    final tag = tagAndCipherText.sublist(0, _aesTagLength);
    final cipherText = tagAndCipherText.sublist(_aesTagLength);
    final cipherTextAndTag = Uint8List.fromList([...cipherText, ...tag]);
    final cipher = GCMBlockCipher(AESEngine());
    cipher.init(
      false,
      AEADParameters(
        KeyParameter(aesKey),
        _aesTagLength * 8,
        iv,
        Uint8List(0),
      ),
    );
    final plainText = cipher.process(cipherTextAndTag);
    return plainText;
  }

  static AsymmetricKeyPair<PublicKey, PrivateKey> _generateEphemeralKey(
      ECKeyGeneratorParameters ecSpec) {
    final keyGenerator = ECKeyGenerator()
      ..init(ParametersWithRandom(ecSpec, _secureRandom));
    return keyGenerator.generateKeyPair();
  }

  static ECPublicKey _getEcPublicKey(ECDomainParameters params, List<int> senderPubKeyBytes) {
    var Q = params.curve.decodePoint(senderPubKeyBytes);
    return ECPublicKey(Q, params);
  }

  static Uint8List _hkdf(Uint8List uncompressed, Uint8List multiply) {
    final initialKeyMaterial = Uint8List.fromList([...uncompressed, ...multiply]);
    final hkdf = KeyDerivator("SHA-256/HKDF");
    hkdf.init(HkdfParameters(initialKeyMaterial, _secretKeyLength));
    return hkdf.process(Uint8List(0));
  }

  static void validateIVLength(int ivLength) {
    if (ivLength != 12 && ivLength != 16) {
      throw ArgumentError('IV length must be 12 or 16 bytes');
    }
  }

  static ECPrivateKey generateEcPrivateKeyByBigInt(BigInt k){
    final keyParams = ECCurve_secp256r1();
    final ecSpec = ECKeyGeneratorParameters(keyParams);
    // Generate receiver private key
    return ECPrivateKey(k, ecSpec.domainParameters);
  }

  static ECPrivateKey generateEcK1PrivateKeyByBigInt(BigInt k){
    final keyParams = ECCurve_secp256k1();
    final ecSpec = ECKeyGeneratorParameters(keyParams);
    // Generate receiver private key
    return ECPrivateKey(k, ecSpec.domainParameters);
  }

  static ECPrivateKey generateEcPrivateKey(Uint8List k){
    var tmp = bytesToBigInt(k);
    return generateEcPrivateKeyByBigInt(tmp);
  }
  static ECPrivateKey generateEcK1PrivateKey(Uint8List k){
    var tmp = bytesToBigInt(k);
    return generateEcK1PrivateKeyByBigInt(tmp);
  }

  static ECPublicKey generateEcPublicKey(ECPrivateKey privateKey){
    final params = ECCurve_secp256r1();
    BigInt? d = privateKey.d;
    var Q = params.G * d;
    return ECPublicKey(Q, params);
  }
  static ECPublicKey generateEcK1PublicKey(ECPrivateKey privateKey){
    final params = ECCurve_secp256k1();
    BigInt? d = privateKey.d;
    var Q = params.G * d;
    return ECPublicKey(Q, params);
  }

  static Uint8List encrypt2(ECPublicKey publicKey, Uint8List message, {int ivLength = aesIvLength}) {
    validateIVLength(ivLength);
    //
    final ecSpec = ECKeyGeneratorParameters(ECCurve_secp256r1());
    final ephemeralKeyPair = _generateEphemeralKey(ecSpec);
    ECPrivateKey ephemeralPrivKey = ephemeralKeyPair.privateKey as ECPrivateKey;
    ECPublicKey ephemeralPubKey = ephemeralKeyPair.publicKey as ECPublicKey;

    // Generate receiver PK
    // ECPublicKey ecPublicKey = _getEcPublicKey(ecSpec.domainParameters, publicKey);
    ECPublicKey ecPublicKey = publicKey;

    // Derive shared secret
    final uncompressed = ephemeralPubKey.Q!.getEncoded(false);
    final multiply = ecPublicKey.Q! * ephemeralPrivKey.d;
    final aesKey = _hkdf(uncompressed, multiply!.getEncoded(false));
    return _aesEncrypt(message, ephemeralPubKey, aesKey, ivLength: ivLength);
  }

  static Uint8List decrypt2(ECPrivateKey privateKey, Uint8List message, {int ivLength = aesIvLength}) {
    validateIVLength(ivLength);
    final keyParams = ECCurve_secp256r1();
    final ecSpec = ECKeyGeneratorParameters(keyParams);

    // Generate receiver private key
    // final receiverSK = bytesToBigInt(privateKey);
    // ECPrivateKey ecPrivateKey = ECPrivateKey(receiverSK, ecSpec.domainParameters);
    ECPrivateKey ecPrivateKey = privateKey;

    final senderPK = message.sublist(0, _uncompressedPublicKeySize);
    final Q = ecSpec.domainParameters.curve.decodePoint(senderPK);
    final senderPK2 = ECPublicKey(Q, ecSpec.domainParameters);

    // Decapsulate
    final uncompressed = senderPK2.Q!.getEncoded(false);
    final multiply = senderPK2.Q! * ecPrivateKey.d;
    final aesKey = _hkdf(uncompressed, multiply!.getEncoded(false));

    return _aesDecrypt(message, aesKey, ivLength: ivLength);
  }

  static Uint8List generateSeed(){
    var mnemonic = bip39.generateMnemonic();
    return bip39.mnemonicToSeed(mnemonic);
  }

  static List<String> generateMnemonic(){
    return bip39.generateMnemonic().split(" ");
  }

  static Uint8List mnemonicToSeed(List<String> mnemonic){
    return bip39.mnemonicToSeed(mnemonic.join(''));
  }
}


extension ECPublicKeyExtension on ECPublicKey {

    String hexKey(){
      var q = Q!;
      var x = q.x!.toBigInteger()!.toRadixString(16);
      var y = q.y!.toBigInteger()!.toRadixString(16);
      return "04$x$y";
    }

    Uint8List getEncoded({bool compressed = true}){
      return Q!.getEncoded(compressed);
    }

}

extension ECPrivateKeyExtension on ECPrivateKey {

    String hexKey(){
      return d!.toRadixString(16);
    }

    Uint8List getEncoded(){
      return Uint8List.fromList(
          HEX.decode(hexKey().padLeft(32, "0"))
      );
    }

}


void main(){
  var mnemonic = Secp256R1Util.generateMnemonic();
  var seed = Secp256R1Util.mnemonicToSeed(mnemonic);
  var privateKey = Secp256R1Util.generateEcK1PrivateKey(seed);
  var publicKey = Secp256R1Util.generateEcPublicKey(privateKey);
  var plainText = "awdawdada";
  var plainEncode = utf8.encode(plainText);
  var encodeMsg = Secp256R1Util.encrypt2(publicKey, plainEncode);
  var res = Secp256R1Util.decrypt2(privateKey, encodeMsg);
  print(utf8.decode(res));

}
